iT邦幫忙

2024 iThome 鐵人賽

DAY 8
1

https://ithelp.ithome.com.tw/upload/images/20240920/20117461VeBOFiz56t.jpg

介紹

在 Vue 3 中,Composition API 是一種更靈活的狀態管理方式,特別是當應用變得更加複雜時。Pinia 完全支持 Composition API,這意味著我們可以使用 Composition API 來處理狀態邏輯。這篇文章將介紹如何在 Vue 中結合 Pinia 和 Composition API 來實現複雜的狀態管理。

前面幾個單元,給大家介紹比較基本的 vue 應用還有常見方式。現在從這一週的單元開始,會探討較為複雜且會展示一般寫 pinia 的開發人員的 anti pattern

步驟 1:延續先前 pinia 探討,(前情提要)

先前 Day4,我們定義一個 store, (檔案: src/stores/counter.ts)

import { defineStore } from 'pinia';
import { shallowRef, computed } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // state::
  const count = shallowRef(0);

  // 定義 getters 作為衍生狀態
  const doubleCount = computed(() => count.value * 2);

  // 定義 actions 用於修改狀態
  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  // 返回狀態、getters 和 actions
  return { count, doubleCount, increment, decrement };
});

這裡,我們將 count 狀態定義為 shallowRef,並使用 computed 來創建一個衍生狀態 doubleCount。在 actions 中,我們定義了 incrementdecrement 方法來修改狀態。

在組件中使用 Pinia 和 Composition API

現在,我們可以在 Vue 組件中使用這個 Pinia store,並結合 Composition API 來管理狀態和邏輯。
(檔案: src/components/CounterComponent.vue)

<script lang="ts" setup>
import { useCounterStore } from '../stores/counter';

const counterStore = useCounterStore();
</script>

<template>
  <div>
    <p>當前計數:{{ counterStore.count }}</p>
    <p>雙倍計數:{{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment">增加</button>
    <button @click="counterStore.decrement">減少</button>
  </div>
</template>

這裡,我們使用 useCounterStore 來調用 Pinia store,並將狀態和方法解構出來,然後在模板中使用它們。

步驟 2:深入探討 狀態

在先前範例中我們要抓取 pinia 的 state 的部分,我們要引入 pinia 整個 store

import { useCounterStore } from '../stores/counter';
const counterStore = useCounterStore();

並且要使用 state 時會特別麻煩,總是需要夾帶 counterStore 才可使用

{{ counterStore.count }}

pinia 有提供 storeToRefs 可以成功解構出 state 並且使用上和 ref 或是 shallowRef 無異。

所以以上 前情提要 的部分可以修改為

<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '../stores/counter';

const { count, doubleCount } = storeToRefs(useCounterStore());
const { increment, decrement } = useCounterStore();

</script>

<template>
  <div>
    <p>當前計數:{{ count }}</p>
    <p>雙倍計數:{{ doubleCount }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">減少</button>
  </div>
</template>

是不是簡化很多呢?

步驟 3:打臉 anti pattern 到頂級 pinia 操作之流:(單向數據流)

步驟 2 展示 如何使用 storeToRefs,而有一招半式會用 pinia 的開發人員大部分學到這裡就會覺得足夠了,可以處理幾乎所有數據的部分。

我們 步驟 3 要展示一般開發人員在 pinia 的使用上不良的操作習慣並且展示如何改善。
我沿用 步驟 2 最後的例子,並加以變化:
(檔案: src/components/CounterComponent.vue)

<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '../stores/counter';

const { count, doubleCount } = storeToRefs(useCounterStore());
const { increment, decrement } = useCounterStore();
// 這裡我們在 component 寫一個方法,進行更新 count
const doubleCountInComponent = () => {
  count.value *= 2;
}
</script>

<template>
  <div>
    <p>當前計數:{{ count }}</p>
    <p>雙倍計數:{{ doubleCount }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">減少</button>
    <button @click="doubleCountInComponent">變兩倍!!</button>
  </div>
</template>

wrong solution content

由上述程式碼可知,我們在 CounterComponent 內增加一個新方法, doubleCountInComponent
去干涉 count 在 pinia 內的狀態。這樣做是可以動且合法的。

但,為何我們要准許這件事合法

因為 vue 非常自由,讓開發人員可以做這樣的操作,也因此在業界產生成千上萬的爛扣(後續天數會有更多例子),

這樣寫的問題是什麼呢?: 我們舉個例子


假設我們建立了 100 ,1000 像這樣的元件,可能那些元件會行使某些功能去更動到 useCounterStorecount 也許更動的方式是某些元件裡的 ref 因為 watch 順便更新了 count 還是寫了一個複雜的非同步動作,更新了 count ,如果只有一個還好判斷,如果事件驅動一整串粽子 讓某個狀態 A 變成 B 又變成 A 並且在非同步執行結束又讓 A 變成 C,那我們怎判斷是哪裡的 bug ?

是哪個元件?還是寫在哪個 composables 裡面?


為了確保狀態可做管理,所以我建議 所有資料的更新請在 pinia 的 store method 裡面做

因此我們可以把以上程式碼改成這樣

(檔案: src/stores/counter.ts)

import { defineStore } from 'pinia';
import { shallowRef, computed } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // state::
  const count = shallowRef(0);

  // 定義 getters 作為衍生狀態
  const doubleCount = computed(() => count.value * 2);

  // 定義 actions 用於修改狀態
  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }
  // 我們把方法寫道 pinia 的 store 裡面
  const doubleCountInComponent = () => {
    count.value *= 2;
  }

  // 返回狀態、getters 和 actions
  return { count, doubleCount, increment, decrement };
});

(檔案: src/components/CounterComponent.vue)

<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '../stores/counter';

const { count, doubleCount } = storeToRefs(useCounterStore());
const { increment, decrement, doubleCountInComponent } = useCounterStore(); // <-- 我們引用 doubleCountInComponent
// 這時我們這個方法就不需要了
// const doubleCountInComponent = () => {
//  count.value *= 2;
//}
</script>

<template>
  <div>
    <p>當前計數:{{ count }}</p>
    <p>雙倍計數:{{ doubleCount }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">減少</button>
  </div>
</template>

這樣並沒有結束,我們仍然無法預期同事會不會照規範走,也許有其他地方你的同事在 compoent 去進行操作 state 本身的操作

樹大必有枯枝,人多必有白癡

因此我們要多一個強而有力的規範確保單向資料流的操作,正好 vue 原生有提供可以確保資料是唯獨狀態的方法
readonly

因此我們可以把 Pinia Store 的部分改成

(檔案: src/stores/counter.ts)

import { defineStore } from 'pinia';
import { shallowRef, computed, readonly } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // state::
  const count = shallowRef(0);

  // 定義 getters 作為衍生狀態
  const doubleCount = computed(() => count.value * 2);

  // 定義 actions 用於修改狀態
  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }
  // 我們把方法寫道 pinia 的 store 裡面
  const doubleCountInComponent = () => {
    count.value *= 2;
  }

  // 返回狀態、getters 和 actions
  return { 
    count: readonly(count), 
    // 加了這行後,這樣有開發人員在使用 pinia 以外的地方更改 state 就會跳出警告,且無法更動
    doubleCount, 
    increment, 
    decrement 
  };
});

完成單向資料流。

步驟 4:composable 與 pinia 的選擇

有些開發人員,會在 composablepinia之間選擇,不外乎是考慮幾個要點

  1. 是否該狀態多組件是否共用?
  2. 這個專案是否是會擴展?
  3. 狀態是否維持?

這些狀態都是使用 pinia 的標準。

不過我們必須要謹慎且更近一步的去思考這些問題

  1. 通常我們在專案開發時,會有共用的部分,所以把未來可能擴展或可能共用的部分先寫成 pinia,不虧。
  2. 通常會,不要小看客戶的力量。
  3. 這才會是我們使用 pinia 與否的關鍵。

個人使用 pinia store 的哲學在於,管理狀態如其名 pinia 屬於 state management的部分

同時也要想到 vue 提供的 dev tool 是可以監測 pinia 的狀態的,這樣可以減少很多除錯的時間,包含更改狀態時可以直接對 dev tool 做操作(如圖)

devtool sample

另一個部分是:

用過即丟的狀態 -> 我們會選擇 composables
需要保持(或可能需要保持)的狀態 -> 我們會選擇 pinia

結論

通過結合 Pinia 和 Vue 的 Composition API,我們可以靈活且高效地管理應用中的複雜狀態。使用 Composition API 讓我們能夠更自然地組織狀態和邏輯,Pinia 的簡潔 API 提供了更好的開發體驗。

這篇文章展示了如何使用 Composition API 來定義 Pinia store,並涵蓋了如何處理異步操作和模組化管理。希望這篇文章能幫助你更好地理解 Vue 中的狀態管理。


上一篇
Day 7: 深入 Vue 組件間的通訊:使用 Props 和 Emit 傳遞數據與事件
下一篇
Day 9: 高階組件設計:使用 Zod 和 Vee-Validate 進行動態表單驗證
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言